package template

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"log"
	"net/http"
	"os"
	"path/filepath"
)

type (
	FsLoader struct {
		fs fs.FS
	}

	// FilesystemLoader 从文件系统中加载
	FilesystemLoader struct {
		baseDir string
	}

	HttpFilesystemLoader struct {
		fs      http.FileSystem
		baseDir string
	}
)

/** ======================== FsLoader ================================= */

func NewFsLoader(fs fs.FS) *FsLoader {
	return &FsLoader{
		fs: fs,
	}
}

func (l *FsLoader) Abs(base, name string) string {
	return filepath.Join(filepath.Dir(base), name)
}

func (l *FsLoader) Get(path string) (io.Reader, error) {
	return l.fs.Open(path)
}

/** ======================== FilesystemLoader ================================= */

func MustNewLocalFileSystemLoader(baseDir string) *FilesystemLoader {
	loader, err := NewLocalFileSystemLoader(baseDir)
	if err != nil {
		log.Panic(err)
	}
	return loader
}

func NewLocalFileSystemLoader(baseDir string) (*FilesystemLoader, error) {
	f := &FilesystemLoader{}
	if baseDir != "" {
		if err := f.setBaseDir(baseDir); err != nil {
			return nil, err
		}
	}
	return f, nil
}

func (fs *FilesystemLoader) setBaseDir(path string) error {
	if !filepath.IsAbs(path) {
		abs, err := filepath.Abs(path)
		if err != nil {
			return err
		}
		path = abs
	}

	// Check for existence
	fi, err := os.Stat(path)
	if err != nil {
		return err
	}
	if !fi.IsDir() {
		return fmt.Errorf("the given path '%s' is not a directory", path)
	}

	fs.baseDir = path
	return nil
}

func (fs *FilesystemLoader) Abs(base, name string) string {
	if filepath.IsAbs(name) {
		return name
	}

	// Our own base dir has always priority; if there's none
	// we use the path provided in base.
	var err error
	if fs.baseDir == "" {
		if base == "" {
			base, err = os.Getwd()
			if err != nil {
				panic(err)
			}
			return filepath.Join(base, name)
		}

		return filepath.Join(filepath.Dir(base), name)
	}

	return filepath.Join(fs.baseDir, name)
}

func (fs *FilesystemLoader) Get(path string) (io.Reader, error) {
	buf, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	return bytes.NewReader(buf), nil
}

/** ======================== HttpFilesystemLoader ================================= */

func MustNewHttpFileSystemLoader(httpFs http.FileSystem, baseDir string) *HttpFilesystemLoader {
	loader, err := NewHttpFileSystemLoader(httpFs, baseDir)
	if err != nil {
		log.Panic(err)
	}
	return loader
}

func NewHttpFileSystemLoader(httpFs http.FileSystem, baseDir string) (*HttpFilesystemLoader, error) {
	hfs := &HttpFilesystemLoader{
		fs:      httpFs,
		baseDir: baseDir,
	}
	if httpFs == nil {
		err := errors.New("httpfs cannot be nil")
		return nil, err
	}
	return hfs, nil
}

// Abs in this instance simply returns the filename, since
// there's no potential for an unexpanded path in an http.FileSystem
func (h *HttpFilesystemLoader) Abs(base, name string) string {
	return name
}

// Get returns an io.Reader where the template's content can be read from.
func (h *HttpFilesystemLoader) Get(path string) (io.Reader, error) {
	fullPath := path
	if h.baseDir != "" {
		fullPath = fmt.Sprintf(
			"%s/%s",
			h.baseDir,
			fullPath,
		)
	}

	return h.fs.Open(fullPath)
}
